嵌入式GUI开发 - LVGL
LVGL(Light and Versatile Graphics Library)是一个非常轻量的 GUI 库,因此非常适合运行在 MCU 上,包括运行在 RTOS 和 Bare-Metal 上。当然,由于其比较良好的抽象层能力,其也能运行在 Windows、Linux、WebAssembly 上,在那些只有几十甚至十几兆的精简 Linux 使用 LVGL 开发 UI 成了绝佳选择。
在Qt上运行LVGL
在嵌入式开发调试 GUI 是一件极为繁琐的事。调试不方便、程序烧录费时间。所以一般和平台无关的代码,一般都会选择在 PC 上调试开发,确认其在 PC 上模拟运行没有问题时,再将代码放到嵌入式设备上运行。
这里我们以将 LVGL 运行在 Qt 上为例,一来是展示 LVGL 的研究过程,二来是方便日常 LVGL 开发的过程中,方便调试。下载源代码。
lvgl-9.4.0
├── lv_conf_template.h
├── lvgl.h
└── src
以上就是 LVGL 的代码实现,然后我们需要对其做一些配置。复制 lv_conf_template.h 生成 lv_conf.h,然后将:
#if 0 /*Set it to "1" to enable content*/
将其改为 1,使能该文件进行的宏配置。
组件式开发
使用 LVGL 开发自定义组件时(小到空间,大到页面),推荐使用 LVGL 内部组件实现的面向对象的方式进行开发。这样会带来很多好处。至少有两个明显的好处,一个是增加了组件的可复用性,另一个是增加了代码级别的可维护性,避免到处都是全局/局部变量。
我们以一个自定义组件 lv_cooldown_switch 为例,区别于 LVGL 提供的默认 lv_switch 组件,它在点击后2s内进入disabled状态,2s后恢复。这样在性能较差的芯片上,可以有效防止快速点击 switch 触发系统某个功能频繁开关导致系统卡顿的问题,全部实现如下:
#include "lv_cooldown_switch.h"
#include <src/core/lv_obj_class_private.h>
#include <src/widgets/switch/lv_switch_private.h>
#define MY_CLASS &lv_cooldown_switch_class
typedef struct {
lv_switch_t obj;
lv_timer_t *timer;
} lv_cooldown_switch_t;
static void cooldown_timer_cb(lv_timer_t *timer) {
lv_cooldown_switch_t *self = (lv_cooldown_switch_t *)lv_timer_get_user_data(timer);
lv_obj_remove_state((lv_obj_t *)self, LV_STATE_DISABLED);
lv_timer_pause(timer);
}
static void lv_cooldown_switch_event(const lv_obj_class_t *class_p, lv_event_t *e);
static void lv_cooldown_switch_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj) {
lv_cooldown_switch_t *self = (lv_cooldown_switch_t *)obj;
self->timer = lv_timer_create(cooldown_timer_cb, 2000, self);
lv_timer_pause(self->timer);
}
static void lv_cooldown_switch_destructor(const lv_obj_class_t *class_p, lv_obj_t *obj) {
lv_cooldown_switch_t *self = (lv_cooldown_switch_t *)obj;
if (self->timer != NULL) {
lv_timer_delete(self->timer);
}
}
const lv_obj_class_t lv_cooldown_switch_class = {
.constructor_cb = lv_cooldown_switch_constructor,
.destructor_cb = lv_cooldown_switch_destructor,
.event_cb = lv_cooldown_switch_event,
.width_def = (4 * LV_DPI_DEF) / 10,
.height_def = (4 * LV_DPI_DEF) / 17,
.group_def = LV_OBJ_CLASS_GROUP_DEF_TRUE,
.instance_size = sizeof(lv_cooldown_switch_t),
.base_class = &lv_switch_class,
.name = "lv_cooldown_switch",
};
static void lv_cooldown_switch_event(const lv_obj_class_t *class_p, lv_event_t *e) {
LV_UNUSED(class_p);
lv_res_t res = lv_obj_event_base(MY_CLASS, e);
if (res != LV_RES_OK) return;
lv_obj_t *obj = lv_event_get_target(e);
lv_cooldown_switch_t *self = (lv_cooldown_switch_t *)obj;
if (lv_event_get_code(e) == LV_EVENT_CLICKED) {
lv_obj_add_state(obj, LV_STATE_DISABLED);
lv_timer_reset(self->timer);
lv_timer_resume(self->timer);
}
}
lv_obj_t *lv_cooldown_switch_create(lv_obj_t *parent) {
lv_obj_t *obj = lv_obj_class_create_obj(MY_CLASS, parent);
lv_obj_class_init_obj(obj);
return obj;
}
在实现完上述组件后,我们运行代码时,发现其无法显示,但是点击改区域,通过日志能看到组件功能是正常运行的。
这是因为 LVGL 控件样式应用逻辑的问题。在每个 lv_obj_t 被创建时,lv_obj_class_init_obj() 函数会调用 lv_theme_apply() 应用样式,最终指定的函数为 src/themes/default/lv_theme_default.c 的 theme_apply() 函数(如果我们使用 lv_theme_default)。这个函数应用样式的代码大致为:
if(lv_obj_check_type(obj, &lv_obj_class)) {
// ...
} else if(lv_obj_check_type(obj, &lv_switch_class)) {
// ...
} // ...
而我们自定义的组件类型名为 lv_cooldown_switch_class,自然无法应用任何类型的样式。解决办法就是在 lv_cooldown_switch_constructor() 实现样式设置,或者参考官方文档扩展自定义样式。
扩展自定义主题
承接上节所述,扩展自定义主题成为了一种灵活且必不可少的实现方式。如下是一个简单的实现参考:
#include "custom_theme.h"
#include "lv_cooldown_switch.h"
typedef struct {
lv_style_t bg_color_white;
// ...
} custom_theme_styles_t;
typedef struct {
lv_theme_t theme;
lv_theme_t *parent;
int32_t disp_dpi;
lv_color_t color_grey;
custom_theme_styles_t styles;
// ...
} custom_theme_t;
static void custom_theme_apply_cb(lv_theme_t *th, lv_obj_t *obj) {
custom_theme_t *self = (custom_theme_t *)th->user_data;
if (lv_obj_check_type(obj, &lv_cooldown_switch_class)) {
// ...
}
}
static void style_init_reset(lv_style_t *style) {
if (custom_theme_is_inited()) {
lv_style_reset(style);
} else {
lv_style_init(style);
}
}
static void style_init(custom_theme_t *theme) {
theme->color_grey = theme->parent->flags & MODE_DARK ? DARK_COLOR_GREY : LIGHT_COLOR_GREY;
style_init_reset(&theme->styles.bg_color_white);
lv_style_set_bg_color(&theme->styles.bg_color_white, theme->color_card);
lv_style_set_bg_opa(&theme->styles.bg_color_white, LV_OPA_COVER);
lv_style_set_text_color(&theme->styles.bg_color_white, theme->color_text);
// ...
}
static custom_theme_t *__self__ = NULL;
custom_theme_t *custom_theme_default_instance() {
return __self__;
}
bool custom_theme_is_inited() {
return __self__ != NULL;
}
custom_theme_t *custom_theme_create() {
custom_theme_t *self = lv_malloc_zeroed(sizeof(custom_theme_t));
self->parent = lv_display_get_theme(NULL);
if (self->parent == NULL) {
if (lv_theme_default_is_inited()) {
self->parent = lv_theme_default_get();
} else {
self->parent = lv_theme_default_init(NULL, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), false, LV_FONT_DEFAULT);
}
}
self->theme.user_data = self;
self->disp_dpi = lv_display_get_dpi(NULL);
lv_theme_set_parent(&self->theme, self->parent);
lv_theme_set_apply_cb(&self->theme, custom_theme_apply_cb);
lv_display_set_theme(NULL, &self->theme);
style_init(self);
if (__self__ == NULL) {
__self__ = self;
}
return self;
}
void custom_theme_destroy(custom_theme_t *self) {
if (self == NULL) return;
if ((lv_display_get_theme(NULL) == &self->theme) && (self->parent != NULL)) {
lv_display_set_theme(NULL, self->parent);
}
if (__self__ == self) {
__self__ = NULL;
}
lv_free(self);
}
关键还是custom_theme_apply_cb() 这个样式应用函数,例如当我们需要实现 lv_cooldown_switch_class 的样式时,我们可以简单复制 src/themes/default/lv_theme_default.c 中 lv_switch_class 样式的设置,然后再根据需求自定义样式。